-- UltraFX.lua — Effets “Ultra Crit” (Explosion + Ring + Glint), follow + gravité + offsets + délais
-- Cible  : explosion “chaude” (ORANGE/RED) avec délai et spawn à l’endroit du texte
-- Non-cible (AoE) : explosion “bleue” (#1C65CA) avec délai, offset bleu correct en mode cible
-- No-taint: aucune mesure de régions (pas de GetLeft/GetBottom). Hook robuste sur DFFT.showNumber.

local ADDON, DFFT = ...
if not DFFT or not DFFT.DB then return end
local DB = DFFT.DB

-- ========= Utils =========
local function clamp(x,a,b) if x<a then return a elseif x>b then return b else return x end end
local function lerp(a,b,t) return a + (b - a) * t end
local function rgb(c) if not c then return 1,1,1 end return c[1] or 1, c[2] or 1, c[3] or 1 end
local function setRGBA(tex, r, g, b, a) tex:SetVertexColor(r, g, b); tex:SetAlpha(a) end

-- helpers robustes pour valeurs numériques
local function num(v, def)
  v = tonumber(v)
  if v == nil or v ~= v or v == math.huge or v == -math.huge then return def end
  return v
end
local function npos(v, def)  -- >=0
  v = num(v, def)
  if v < 0 then return def end
  return v
end

-- Couleurs
local ORANGE    = DFFT.ORANGE   or {1.00, 0.50, 0.00}
local FIRE_RED  = DFFT.FIRE_RED or {1.00, 0.20, 0.20}
local NONBLUE   = DB.nonTargetColor or {0.45, 0.75, 1.00} -- bleu clair non-cible
local AOE_BLUE  = {0x1C/255, 0x65/255, 0xCA/255}          -- bleu foncé #1C65CA

-- Ressources
local WHITE_TEX = "Interface\\Buttons\\WHITE8x8"

-- ========= Config (défauts si nil) =========
if DB.UltraFXEnabled == nil       then DB.UltraFXEnabled = true end
if DB.UltraFXGlintEnabled == nil  then DB.UltraFXGlintEnabled = true end

-- Profil “cible” (chaud)
DB.UltraFXParticleCount    = DB.UltraFXParticleCount    or 22
DB.UltraFXParticleSizeMin  = DB.UltraFXParticleSizeMin  or 6
DB.UltraFXParticleSizeMax  = DB.UltraFXParticleSizeMax  or 12
DB.UltraFXParticleDurMin   = DB.UltraFXParticleDurMin   or 0.45
DB.UltraFXParticleDurMax   = DB.UltraFXParticleDurMax   or 0.80
DB.UltraFXGravity          = (DB.UltraFXGravity ~= nil) and DB.UltraFXGravity or -700
DB.UltraFXSpeedMin         = DB.UltraFXSpeedMin         or 240
DB.UltraFXSpeedMax         = DB.UltraFXSpeedMax         or 520
DB.UltraFXSpinMax          = DB.UltraFXSpinMax          or 9.0

DB.UltraFXRingCount        = DB.UltraFXRingCount        or 12
DB.UltraFXRingDur          = DB.UltraFXRingDur          or 0.38
DB.UltraFXRingStartR       = DB.UltraFXRingStartR       or 6
DB.UltraFXRingEndR         = DB.UltraFXRingEndR         or 72
DB.UltraFXRingSize         = DB.UltraFXRingSize         or 6

DB.UltraFXGlintDur         = DB.UltraFXGlintDur         or 0.28
DB.UltraFXGlintWidth       = DB.UltraFXGlintWidth       or 120
DB.UltraFXGlintHeight      = DB.UltraFXGlintHeight      or 8
DB.UltraFXGlintAlpha       = DB.UltraFXGlintAlpha       or 0.45

-- Profil “non-cible / AoE” (bleu)
DB.UltraFXAoEParticleCount   = DB.UltraFXAoEParticleCount   or 22
DB.UltraFXAoEParticleSizeMin = DB.UltraFXAoEParticleSizeMin or 5
DB.UltraFXAoEParticleSizeMax = DB.UltraFXAoEParticleSizeMax or 10
DB.UltraFXAoEParticleDurMin  = DB.UltraFXAoEParticleDurMin  or 0.40
DB.UltraFXAoEParticleDurMax  = DB.UltraFXAoEParticleDurMax  or 0.72
DB.UltraFXAoEGravity         = (DB.UltraFXAoEGravity ~= nil) and DB.UltraFXAoEGravity or -760
DB.UltraFXAoESpeedMin        = DB.UltraFXAoESpeedMin        or 260
DB.UltraFXAoESpeedMax        = DB.UltraFXAoESpeedMax        or 560
DB.UltraFXAoESpinMax         = DB.UltraFXAoESpinMax         or 10.0

DB.UltraFXAoERingCount       = DB.UltraFXAoERingCount       or 14
DB.UltraFXAoERingDur         = DB.UltraFXAoERingDur         or 0.34
DB.UltraFXAoERingStartR      = DB.UltraFXAoERingStartR      or 8
DB.UltraFXAoERingEndR        = DB.UltraFXAoERingEndR        or 78
DB.UltraFXAoERingSize        = DB.UltraFXAoERingSize        or 5

-- Suivi du texte (drift) + lead + délais
DB.UltraFXFollowEnabled  = (DB.UltraFXFollowEnabled ~= false)
DB.UltraFXFollowDur      = DB.UltraFXFollowDur        or (DB.durationBig or 1.8)
DB.UltraFXLeadFrac       = (DB.UltraFXLeadFrac ~= nil) and DB.UltraFXLeadFrac or 0.12
DB.UltraFXTargetDelay    = (DB.UltraFXTargetDelay ~= nil) and DB.UltraFXTargetDelay or 0.20
DB.UltraFXBlueDelay      = (DB.UltraFXBlueDelay   ~= nil) and DB.UltraFXBlueDelay   or 0.20
DB.UltraFXMaxActive      = DB.UltraFXMaxActive or 8

-- ========= Anchor helpers (aucune mesure de plate) =========
local function safeNameplateForUnit(unit)
  if not C_NamePlate or not C_NamePlate.GetNamePlateForUnit then return nil end
  if not UnitExists(unit) then return nil end
  local plate = C_NamePlate.GetNamePlateForUnit(unit)
  if plate and plate.IsForbidden and plate:IsForbidden() then return nil end
  return plate
end

-- Position initiale proche de la naissance des chiffres (vbig)
local function computeAnchor(ownerIsTarget)
  local useTargetAnchor = DB.globalAnchorToTarget
  local unit  = useTargetAnchor and "target" or "player"
  local plate = safeNameplateForUnit(unit)

  local x, y, point, relTo, relPoint
  if plate then
    local yOff = (DB.vbigTargetOffsetY or 0) - ((DB.targetHeightComp or 0.40) * 40)
    x = (DB.vbigTargetOffsetX or 0)
    y = yOff
    -- En ancrage cible, appliquer l’offset bleu des non-cibles
    if ownerIsTarget == false then
      x = x + (DB.nonTargetOffsetX or 0)
      y = y + (DB.nonTargetOffsetY or 0)
    end
    point, relTo, relPoint = "TOP", plate, "TOP"
  else
    x = (DB.vbigAnchorX or 0)
    y = (DB.vbigAnchorY or 0)
    if not useTargetAnchor then
      local spread = DB.playerSideSpread or 0
      if ownerIsTarget == false then
        x = x + (spread or 0) + (DB.nonTargetPlayerExtraX or 40)
      else
        x = x + math.abs(spread or 0)
      end
    end
    point, relTo, relPoint = "CENTER", UIParent, "CENTER"
  end

  -- micro-jitter
  x = x + math.random(-2, 2)
  y = y + math.random(-2, 2)

  return point, relTo, relPoint, x, y
end

-- ========= Pool & FX =========
local FXPool, Active = {}, {}

local function acquireFXFrame()
  for i=1,#FXPool do if not FXPool[i]:IsShown() then return FXPool[i] end end
  local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  f:SetSize(2,2); f:SetFrameStrata("HIGH"); f:SetFrameLevel(200); f:SetAlpha(1)

  f.particles = f.particles or {}
  f.t, f.dur = 0, 0

  -- follow
  f.followOn  = false
  f.fxDurRef  = DB.UltraFXFollowDur
  f.basePoint = nil; f.baseRelTo = nil; f.baseRelPoint = nil
  f.baseX, f.baseY = 0, 0
  f.driftDX, f.driftDY = 0, 0

  -- start delay
  f.startDelay = 0
  f.prevTd     = 0
  f.started    = false

  -- glint
  f.glint = f:CreateTexture(nil, "ARTWORK")
  f.glint:SetTexture(WHITE_TEX)
  f.glint:SetBlendMode("ADD")
  f.glint:Hide()
  f._glintOn  = false
  f._glintDur = DB.UltraFXGlintDur
  f._glintT   = 0
  f._glintWanted = false

  -- pool interne — toujours des bornes sûres
  local total = math.max(
    npos(DB.UltraFXAoEParticleCount, 22),
    npos(DB.UltraFXAoERingCount,     14),
    npos(DB.UltraFXParticleCount,    22),
    npos(DB.UltraFXRingCount,        12)
  ) + 8
  for i=1,total do
    local p = f:CreateTexture(nil, "BACKGROUND")
    p:SetTexture(WHITE_TEX); p:SetBlendMode("ADD"); p:Hide()
    p._visible = false; p.x, p.y = 0, 0
    f.particles[#f.particles+1] = p
  end

  FXPool[#FXPool+1] = f
  return f
end

-- Explosion paramétrable (isBlue = palette bleue ?)
local function initExplosion(f, isBlue)
  -- bornes sûres (anti-nil après ResetAll)
  local count  = (isBlue and npos(DB.UltraFXAoEParticleCount, 22)) or npos(DB.UltraFXParticleCount, 22)
  local minS   = (isBlue and npos(DB.UltraFXAoEParticleSizeMin, 5)) or npos(DB.UltraFXParticleSizeMin, 6)
  local maxS   = (isBlue and npos(DB.UltraFXAoEParticleSizeMax,10)) or npos(DB.UltraFXParticleSizeMax,12)
  local minDur = (isBlue and num(DB.UltraFXAoEParticleDurMin, 0.40)) or num(DB.UltraFXParticleDurMin, 0.45)
  local maxDur = (isBlue and num(DB.UltraFXAoEParticleDurMax, 0.72)) or num(DB.UltraFXParticleDurMax, 0.80)
  local vMin   = (isBlue and num(DB.UltraFXAoESpeedMin, 260)) or num(DB.UltraFXSpeedMin, 240)
  local vMax   = (isBlue and num(DB.UltraFXAoESpeedMax, 560)) or num(DB.UltraFXSpeedMax, 520)
  local spinMax= (isBlue and num(DB.UltraFXAoESpinMax,10.0)) or num(DB.UltraFXSpinMax,9.0)
  local grav   = (isBlue and num(DB.UltraFXAoEGravity,-760)) or num(DB.UltraFXGravity,-700)

  -- couleurs
  local c0, c1 = (isBlue and NONBLUE or {1.00,0.85,0.20}), (isBlue and AOE_BLUE or FIRE_RED)
  local r0,g0,b0 = rgb(c0)
  local r1,g1,b1 = rgb(c1)

  local used = 0
  for i=1,count do
    local p = f.particles[i]; if not p then break end
    used = used + 1
    p:ClearAllPoints(); p._visible = false
    p.x, p.y = 0, 0
    p:SetPoint("CENTER", f, "CENTER", p.x, p.y)
    local size = math.random(minS, maxS)
    p:SetSize(size, size)
    setRGBA(p, r0, g0, b0, 0) -- pas de flash

    local ang   = math.random() * math.pi * 2
    local speed = math.random(vMin, vMax)
    p.vx = math.cos(ang) * speed
    p.vy = math.sin(ang) * speed
    p.ax, p.ay = 0, grav

    p.t = 0
    p.dur = lerp(minDur, maxDur, math.random())
    p.s0 = size
    p.s1 = size * 0.66

    p.r0,p.g0,p.b0 = r0,g0,b0
    p.r1,p.g1,p.b1 = r1,g1,b1
    p.a0, p.a1 = 0.85, 0.00

    p.spin = (math.random() * 2 - 1) * spinMax
    p.rot  = math.random() * math.pi
    p:SetRotation(p.rot)
  end
  f._expStartIdx, f._expEndIdx = 1, used
end

-- Ring
local function initRing(f, isBlue)
  local n    = (isBlue and npos(DB.UltraFXAoERingCount, 14)) or npos(DB.UltraFXRingCount, 12)
  local r0   = (isBlue and num(DB.UltraFXAoERingStartR, 8))  or num(DB.UltraFXRingStartR, 6)
  local r1   = (isBlue and num(DB.UltraFXAoERingEndR,   78)) or num(DB.UltraFXRingEndR, 72)
  local size = (isBlue and npos(DB.UltraFXAoERingSize,  5))  or npos(DB.UltraFXRingSize, 6)
  local dur  = (isBlue and num(DB.UltraFXAoERingDur,  0.34)) or num(DB.UltraFXRingDur, 0.38)

  local idxStart = (f._expEndIdx or 0) + 1
  local idxEnd   = idxStart + n - 1

  local c0, c1 = (isBlue and NONBLUE or ORANGE), (isBlue and AOE_BLUE or FIRE_RED)
  local r0c,g0c,b0c = rgb(c0)
  local r1c,g1c,b1c = rgb(c1)

  for i=0,n-1 do
    local p = f.particles[idxStart + i]; if not p then break end
    p:ClearAllPoints()
    p.x, p.y = 0, 0
    p:SetPoint("CENTER", f, "CENTER", p.x, p.y)
    p:Show(); p._visible = true
    p:SetSize(size, size)

    p.ang = (i / n) * (math.pi * 2)
    p.r0R = r0; p.r1R = r1

    p.t = 0; p.dur = dur
    p.s0 = size; p.s1 = size * 0.8

    p.r0c,p.g0c,p.b0c = r0c,g0c,b0c
    p.r1c,p.g1c,p.b1c = r1c,g1c,b1c
    p.a0, p.a1 = 0.55, 0.00

    p.spin, p.rot = 0, 0
    p:SetRotation(0)
    p.vx, p.vy, p.ax, p.ay = 0, 0, 0, 0
  end

  f._ringStartIdx, f._ringEndIdx = idxStart, idxEnd
end

local function initGlint(f, isBlue)
  f._glintWanted = DB.UltraFXGlintEnabled and true or false
  if not f._glintWanted then return end
  local w = npos(DB.UltraFXGlintWidth, 120)
  local h = npos(DB.UltraFXGlintHeight, 8)
  f.glint:ClearAllPoints()
  f.glint:SetPoint("CENTER", f, "CENTER", -w*0.6, 0)
  f.glint:SetSize(w, h)
  f.glint:SetAlpha(0)
  f.glint:SetBlendMode("ADD")
  f.glint:Hide()
  f._glintOn  = false
  f._glintDur = num(DB.UltraFXGlintDur, 0.28)
  f._glintT   = 0
end

-- Follow (drift global du FX parent) + lead
local function initFollow(f, ownerIsTarget, point, relTo, relPoint, bx, by)
  f.basePoint, f.baseRelTo, f.baseRelPoint = point, relTo, relPoint
  f.baseX, f.baseY = bx, by

  if DB.UltraFXFollowEnabled ~= true then
    f.followOn = false
    return
  end

  local diag = DB.diagHUltra or 64
  if (ownerIsTarget == false) and (DB.globalAnchorToTarget) then
    diag = math.abs(diag)
  end
  local lead = DB.UltraFXLeadFrac or 0
  f.driftDX = diag * (1 + lead)
  f.driftDY = (DB.riseY or 80) * (1 + lead)
  f.fxDurRef = DB.UltraFXFollowDur or (DB.durationBig or 1.8)
  f.followOn = true
end

local function startUltraFX(ownerIsTarget, isBlue)
  -- Budget avec priorité cible
  local countActive = 0
  for _,fx in ipairs(Active) do if fx:IsShown() then countActive = countActive + 1 end end
  if countActive >= npos(DB.UltraFXMaxActive, 8) and not ownerIsTarget then
    return
  end

  local f = acquireFXFrame()
  f:Hide()
  f.t, f.dur = 0, math.max(
    (isBlue and num(DB.UltraFXAoEParticleDurMax, 0.72) or num(DB.UltraFXParticleDurMax, 0.80)),
    (isBlue and num(DB.UltraFXAoERingDur,        0.34) or num(DB.UltraFXRingDur,        0.38))
  )
  f._expStartIdx, f._expEndIdx = 0, -1
  f._ringStartIdx, f._ringEndIdx = 0, -1
  f.started    = false
  f.prevTd     = 0
  f._glintOn   = false

  -- délai : chaud pour cible, bleu pour non-cible
  f.startDelay = (isBlue and num(DB.UltraFXBlueDelay, 0.20))
              or ((ownerIsTarget and num(DB.UltraFXTargetDelay, 0.20)) or 0)

  local p, relTo, rp, bx, by = computeAnchor(ownerIsTarget)
  f:ClearAllPoints()
  f:SetPoint(p, relTo, rp, bx, by)
  f:SetFrameLevel(200)

  initFollow(f, ownerIsTarget, p, relTo, rp, bx, by)
  initExplosion(f, isBlue)
  initRing(f, isBlue)
  initGlint(f, isBlue)

  f:SetScript("OnUpdate", function(_, elapsed)
    f.t = f.t + elapsed
    local T = f.t

    -- follow : on suit même pendant le délai pour coller au texte
    if f.followOn and f.baseRelTo and f.baseRelTo:IsShown() then
      local dur = (f.fxDurRef and f.fxDurRef > 0) and f.fxDurRef or 1.0
      local u = clamp(T / dur, 0, 1)
      local dx = f.driftDX * u
      local dy = f.driftDY * u
      f:ClearAllPoints()
      f:SetPoint(f.basePoint, f.baseRelTo, f.baseRelPoint, f.baseX + dx, f.baseY + dy)
    end

    -- timeline “effets” avec délai de départ
    local Td = T - (f.startDelay or 0); if Td < 0 then Td = 0 end
    local dStep = Td - (f.prevTd or 0); f.prevTd = Td

    -- start réel : on montre particules + glint à Td>0
    if (not f.started) and (Td > 0) then
      f.started = true
      if DB.UltraFXGlintEnabled and f._glintDur and f._glintDur > 0 then
        f.glint:Show(); f._glintOn = true; f._glintT  = 0
      end
      for i=f._expStartIdx, f._expEndIdx do
        local p = f.particles[i]; if p then p._visible = true; p:Show() end
      end
      for i=f._ringStartIdx, f._ringEndIdx do
        local p = f.particles[i]; if p then p._visible = true; p:Show() end
      end
    end

    -- Pas encore démarré ? on attend
    if not f.started then return end

    -- Explosion
    for i=f._expStartIdx, f._expEndIdx do
      local p = f.particles[i]; if p and p._visible and p:IsShown() then
        p.t = p.t + dStep
        local u = clamp(p.t / p.dur, 0, 1)

        p.vx = p.vx + p.ax * dStep
        p.vy = p.vy + p.ay * dStep
        p.x  = p.x  + p.vx * dStep
        p.y  = p.y  + p.vy * dStep

        p:ClearAllPoints()
        p:SetPoint("CENTER", f, "CENTER", p.x, p.y)

        local s = lerp(p.s0, p.s1, u)
        p:SetSize(s, s)

        local r = lerp(p.r0, p.r1, u)
        local g = lerp(p.g0, p.g1, u)
        local b = lerp(p.b0, p.b1, u)
        local a = lerp(p.a0, p.a1, u)
        setRGBA(p, r, g, b, a)

        p.rot = p.rot + p.spin * dStep
        p:SetRotation(p.rot)

        if u >= 1 then p:Hide(); p._visible = false end
      end
    end

    -- Ring
    for i=f._ringStartIdx, f._ringEndIdx do
      local p = f.particles[i]; if p and p._visible and p:IsShown() then
        p.t = p.t + dStep
        local u = clamp(p.t / p.dur, 0, 1)

        local R = lerp(p.r0R, p.r1R, u)
        local x = math.cos(p.ang) * R
        local y = math.sin(p.ang) * R

        p:ClearAllPoints()
        p:SetPoint("CENTER", f, "CENTER", x, y)

        local s = lerp(p.s0, p.s1, u)
        p:SetSize(s, s)

        local r = lerp(p.r0c, p.r1c, u)
        local g = lerp(p.g0c, p.g1c, u)
        local b = lerp(p.b0c, p.b1c, u)
        local a = lerp(p.a0, p.a1, u)
        setRGBA(p, r, g, b, a)

        if u >= 1 then p:Hide(); p._visible = false end
      end
    end

    -- Glint
    if f._glintOn then
      f._glintT = f._glintT + dStep
      local u = clamp(f._glintT / (f._glintDur or 0.28), 0, 1)
      local w = npos(DB.UltraFXGlintWidth, 120)
      local x = lerp(-w*0.6, w*0.6, u)
      f.glint:ClearAllPoints()
      f.glint:SetPoint("CENTER", f, "CENTER", x, 0)
      local a = math.sin(u * math.pi) * (num(DB.UltraFXGlintAlpha, 0.45))
      f.glint:SetAlpha(a)
      if u >= 1 then f.glint:Hide(); f._glintOn = false end
    end

    -- fin
    if Td >= f.dur then
      for i=1,#f.particles do f.particles[i]:Hide(); f.particles[i]._visible = false end
      f.glint:Hide(); f._glintOn = false
      f:SetScript("OnUpdate", nil)
      f:Hide()
    end
  end)

  f:Show()
  Active[#Active+1] = f
end

-- ========= Détection Ultra =========
local function _isHotUltra(tier, ownerIsTarget, isCrit)
  return ownerIsTarget == true and isCrit and tier == "vbig"
end

local function _isBlueUltra(tier, ownerIsTarget, isCrit)
  if ownerIsTarget ~= false then return false end
  if not isCrit then return false end
  -- Non-cible : vbig ou (big & crit) => on déclenche bleu
  return (tier == "vbig") or (tier == "big")
end

-- ========= Hook robuste =========
local function InstallUltraFXHook()
  if DFFT._UltraFX_wrapped then return end
  local orig = DFFT.showNumber
  DFFT._UltraFX_origShowNumber = orig
  DFFT.showNumber = function(amount, text, tier, ownerIsTarget, isCrit, sideDirForced)
    local ret = orig(amount, text, tier, ownerIsTarget, isCrit, sideDirForced)
    if not DB.UltraFXEnabled or not isCrit then return ret end
    if _isHotUltra(tier, ownerIsTarget, isCrit) then
      startUltraFX(true, false)
    elseif _isBlueUltra(tier, ownerIsTarget, isCrit) then
      startUltraFX(false, true)
    end
    return ret
  end
  DFFT._UltraFX_wrapped = true
end

-- installe à coup sûr, même si l’ordre du .toc change
local ev = CreateFrame("Frame")
ev:RegisterEvent("ADDON_LOADED")
ev:RegisterEvent("PLAYER_LOGIN")
ev:SetScript("OnEvent", function(_, e, name)
  if e == "ADDON_LOADED" and name == ADDON then
    InstallUltraFXHook()
  elseif e == "PLAYER_LOGIN" then
    InstallUltraFXHook()
  end
end)
